JPA와 REST API로 댓글 기능 만들기 2

✒️ 2025-05-28 13:10 내용 수정

스프링부트3 자바 백엔드 개발입문 내용 참고 및 정리



1. 댓글 조회

  1. 먼저 댓글을 조회하기 위해선 JPA로 DB Read 수행하기에서 Controller가 src/main/resources/templates 폴더 내의 mustache파일을 반환했다는 것을 떠올려보자.
    • 이를 위해 src/main/resources/templates 폴더에 comments라는 폴더를 생성하고, 폴더 내에 _comments.mustache 파일을 생성한다.
    • Mustache 참고.

jpa_comment_view 1.png

  1. _comments.mustache는 게시글 페이지에서 댓글 목록 및 생성을 보여줄 항목이므로, 게시글 상세보기 페이지인 show.mustache 파일에서 _comments.mustache 파일을 출력하도록 설정한다.
<!doctype html>  
<html lang="ko">  
<head>  
    <meta charset="UTF-8">  
    <meta name="viewport"  
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">  
    <meta http-equiv="X-UA-Compatible" content="ie=edge">  
	<!-- bootstrap code 생략 -->
    <title>SpringBoot Example</title>  
</head>  
<body>  
{{> layouts/header }}  
  
<!-- 중략 -->
  
{{! 댓글 파일 }}  
{{> comments/_comments }}  
  
{{> layouts/footer }}  
  
<script>  
    function check() { // 삭제 확인용  
        if (confirm("삭제하시겠습니까?") != false) {  
            location.href='/articles/{{article.id}}/delete';  
        }  
    }  
</script>  
</body>  
</html>
  1. _comments.mustache댓글 기능을 담당하는 View 역할로, 댓글 목록과 댓글 추가 기능을 위한 View 파일인 _list.mustache 파일과 _new.mustache 파일을 src/main/resources/templates/comments 폴더 내에 추가한다.
    • _comments.mustache_list.mustache 파일과 _new.mustache 파일을 가져오는 코드를 추가한다.
    • 댓글 조회에선 _list.mustache 파일만 먼저 작성한다.

jpa_comment_view 2.png

{{! _comments.mustache }}
<div>  
    {{> comments/_list }}  
    {{> comments/_new }}  
</div>
  1. _list.mustache에서 댓글을 보여주기 위한 영역을 설정한다.
<div id="comments-list">  
    {{! card 영역에선 DTO의 field를 사용하도록 설정 }}
    {{#commentDtos}}  
        <div class="card m-2" id="comments-{{id}}">  
            <div class="card-header">  
                {{nickname}}  
            </div>  
            <div class="card-body">  
                {{body}}  
            </div>  
        </div>  
    {{/commentDtos}}  
</div>
  1. 이제 View에 데이터를 전달해줄 Model을 생성하기 위해 ArticleController 클래스의 메소드를 수정한다.
package com.example.demo.controller;  
  
import com.example.demo.DTO.ArticleForm;  
import com.example.demo.DTO.CommentDto;  
import com.example.demo.entity.Article;  
import com.example.demo.entity.Comment;  
import com.example.demo.repository.ArticleRepository;  
import com.example.demo.service.CommentService;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Controller;  
import org.springframework.ui.Model;  
import org.springframework.web.bind.annotation.*;  
import org.springframework.web.servlet.mvc.support.RedirectAttributes;  
  
import java.util.Iterator;  
import java.util.List;  
import java.util.Optional;  
  
@Controller  
@RequestMapping("/articles")  
@Slf4j // simple logging facade for java  
public class ArticleController {  
  
    @Autowired  
    private ArticleRepository articleRepository;  
  
    @Autowired  
    private CommentService commentService;  
  
    // ...생략
  
	@GetMapping("{id}")  // 경로 매개변수 사용  
	public String show(@PathVariable Long id, Model model) {  
	    log.info("id = " + id);  // 로그 확인  
	    Article articleEntity = articleRepository.findById(id).orElse(null);  // id로 조회  
	    List<CommentDto> commentDtos = commentService.comments(id); // 댓글 가져오기  
	    model.addAttribute("article", articleEntity);  
	    model.addAttribute("commentDtos", commentDtos);  
	    return "/articles/show";  // id로 조회할 데이터를 보여줄 view
	}
  
    // ...생략
}
  1. 이제 브라우저에 http://localhost:port/로 접속한 후 댓글이 작성된 더미 데이터의 게시글 상세보기 페이지로 이동해 댓글을 가져왔는지 확인한다.

jpa_comment_view 3.png


2. 댓글 추가

  1. 앞서 위에서 생성해둔 _new.mustache 파일에 댓글 추가를 위한 내용을 구성한다.
{{! _new.mustache }}
<div class="card m-2" id="comment-new">  
    <div class="card-body">  
        <form>  
            <!-- 닉네임 입력 -->  
            <div class="mb-3">  
                <label for="new-comment-nickname" class="form-label">닉네임</label>  
                <input type="text" class="form-control" id="new-comment-nickname">  
            </div>  
            <!-- 댓글 본문 -->  
            <div class="mb-3">  
                <label for="new-comment-body" class="form-label">댓글 내용</label>  
                <textarea class="form-control" rows="3" id="new-comment-body"></textarea>  
            </div>  
            {{! 댓글의 articleId를 넘겨주기 위한 hidden input }}            
            {{! ArticleController의 show에서 model은 article과 commentDtos를 View에 보내줌 }}  
            {{#article}}  
                <input type="hidden" id="new-comment-article-id" value="{{id}}">  
            {{/article}}  
            <button type="button" class="btn btn-primary" id="comment-create-btn">작성</button>  
        </form>  
    </div>  
</div>  
  
<script>  
{  
    // button 가져오기  
    const commentCreateBtn = document.querySelector("#comment-create-btn");  
    // click event listener 추가  
    commentCreateBtn.addEventListener("click", function() {  
        // input 내용으로 comment 객체 생성  
        const comment = {  
            nickname: document.querySelector("#new-comment-nickname").value,  
            articleId: document.querySelector("#new-comment-article-id").value,  
            body: document.querySelector("#new-comment-body").value,  
        }  
        // fetch로 REST API 요청  
        let url = `/api/articles/${comment.articleId}/comments`; // url 설정  
        fetch(url, {  
            method: "POST",  
            headers: {  
                "Content-Type": "application/json"  
            },  
            body: JSON.stringify(comment) // 객체 -> JSON 변환  
        }).then(res => {  
            const msg = (res.ok) ? "댓글이 등록됐습니다." : "댓글 등록 실패";  
            alert(msg);  
			if (res.ok) {  
			    window.location.reload(); // 새로고침  
			}
        });  
    });  
}  
</script>

jpa_comment_view 4.png

jpa_comment_view 5.png


3. 댓글 수정

  1. 이번엔 댓글을 수정하기 위해 _list.mustache 파일에 bootstrap 모달(Modal)과 _new.mustache에서 사용한 <form>을 가져와 수정 데이터를 받을 영역을 추가한다.
    • bootstrap modal : Bootstrap modal
    • Modal을 여는 버튼은 댓글마다 존재하지만, Modal은 1개만 사용하기 때문에 Modal에 id와 articleId를 넣어줄 때 button에 지정한 data- 속성을 사용하여 정보를 저장한다.
    • Javascript의 querySelector()로 값을 <input type="hidden">인 input에 넣어서 fetch()로 보낸다.
<!-- _list.mustache -->
<div id="comments-list">  
    {{! card 영역에선 DTO의 field를 사용하도록 설정 }}  
    {{#commentDtos}}  
        <div class="card m-2" id="comments-{{id}}">  
            <div class="card-header">  
                {{nickname}}  
                <!-- Modal 여는 버튼 -->  
                <button type="button"  
                        class="btn btn-sm btn-outline-primary mx-2"  
                        data-bs-toggle="modal"  
                        data-bs-target="#comment-edit-modal"  
                        data-bs-id="{{id}}"   
						data-bs-article-id="{{articleId}}"  
                        data-bs-nickname="{{nickname}}"  
                        data-bs-body="{{body}}"  
                >  
                    수정  
                </button>  
            </div>  
            <div class="card-body">  
                {{body}}  
            </div>  
        </div>  
    {{/commentDtos}}  
</div>  
  
<!-- Modal -->  
<div class="modal fade" id="comment-edit-modal" tabindex="-1"  
     aria-labelledby="updateModalLabel" aria-hidden="true">  
    <div class="modal-dialog">  
        <div class="modal-content">  
            <div class="modal-header">  
                <h1 class="modal-title fs-5" id="updateModalLabel">댓글 수정</h1>  
                <button type="button" class="btn-close"  
                        data-bs-dismiss="modal" aria-label="Close"></button>  
            </div>  
            <div class="modal-body">  
                <!-- 댓글 수정 form -->                
                <form>  
                    <!-- 닉네임 입력 -->  
                    <div class="mb-3">  
                        <label for="edit-comment-nickname"  
                               class="form-label">닉네임</label>  
                        <input type="text"  
                               class="form-control" id="edit-comment-nickname">  
                    </div>  
                    <!-- 댓글 본문 -->  
                    <div class="mb-3">  
                        <label for="edit-comment-body"  
                               class="form-label">댓글 내용</label>  
                        <textarea class="form-control"  
                                  rows="3" id="edit-comment-body"></textarea>  
                    </div>  
                    <!-- 댓글 id와 articleId 전달 -->  
                    <input type="hidden"  
                           id="edit-comment-id">  
                    <input type="hidden"  
                           id="edit-comment-article-id">  
                    <button type="button" class="btn btn-primary"  
                            id="comment-update-btn">수정완료</button>  
                </form>  
            </div>  
        </div>  
    </div>  
</div>
  1. 이제 Javascript를 사용하여 Modal의 이벤트 처리를 추가하고, fetch()로 REST API를 호출하여 수정 데이터를 보낼 수 있도록 <script>를 작성한다.
Event 설명
hide.bs.modal Modal이 숨겨지기 직전에 실행
hidden.bs.modal Modal이 숨겨진 후 실행
show.bs.modal Modal이 보여지기 직전 실행
shown.bs.modal Modal이 보여진 후 실행
<!-- _list.mustache -->
<div>
	<!-- 위에서 작성해서 생략 -->
</div>

<!-- modal evnet -->
<script>  
{  
    // modal 선택  
    const commentEditModal = document.querySelector("#comment-edit-modal");  
    commentEditModal.addEventListener("show.bs.modal", function(event) {  
        // event = show.bs.modal  
        // event.target = modal  
  
        // 트리거 버튼 선택  
        const triggerBtn = event.relatedTarget;  // modal 여는 버튼
        
        // 데이터 가져오기  
        const id = triggerBtn.getAttribute("data-bs-id");  
        const articleId = triggerBtn.getAttribute("data-bs-article-id");  
        const nickname = triggerBtn.getAttribute("data-bs-nickname");  
        const body = triggerBtn.getAttribute("data-bs-body");  
        
        // 데이터를 modal form에 반영  
        document.querySelector("#edit-comment-id").value = id;  
        document.querySelector("#edit-comment-article-id").value = articleId;  
        document.querySelector("#edit-comment-nickname").value = nickname;  
        document.querySelector("#edit-comment-body").value = body;  
    });  
}  
{  
    // 수정 요청 보내기  
    const commentUpdateBtn = document.querySelector("#comment-update-btn");  
    commentUpdateBtn.addEventListener("click", function() {  
        // 댓글 객체 생성  
        const comment = {  
            id: document.querySelector("#edit-comment-id").value,  
            articleId: document.querySelector("#edit-comment-article-id").value,  
            nickname: document.querySelector("#edit-comment-nickname").value,  
            body: document.querySelector("#edit-comment-body").value  
        }  
        
        // 수정 url 생성  
        const url = `/api/comments/${comment.id}`;  
        // fetch  
        fetch(url, {  
            method: "PATCH", // 수정을 위한 PATCH 요청  
            headers: {  
                "Content-Type": "application/json"  
            },  
            body: JSON.stringify(comment) // 객체 -> JSON 변환  
        }).then(res => {  
            const msg = (res.ok) ? "댓글이 수정됐습니다." : "댓글 수정 실패";  
            alert(msg);  
            if (res.ok) {  
                window.location.reload(); // 새로고침  
            }  
        });  
    });  
}  
</script>

jpa_comment_view 6.png

jpa_comment_view 7.png

jpa_comment_view 8.png

jpa_comment_view 9.png

jpa_comment_view 10.png


4. 댓글 삭제

  1. 댓글 수정에서 만든 Modal을 여는 버튼 코드 바로 아래에 댓글 삭제를 위한 button을 추가한다.
<!-- _list.mustache -->
<div id="comments-list">  
    {{! card 영역에선 DTO의 field를 사용하도록 설정 }}  
    {{#commentDtos}}  
        <div class="card m-2" id="comments-{{id}}">  
            <div class="card-header">  
                {{nickname}}  
                <!-- Modal 여는 버튼 -->  
                <button type="button"  
                        class="btn btn-sm btn-outline-primary mx-2"  
                        data-bs-toggle="modal"  
                        data-bs-target="#comment-edit-modal"  
                        data-bs-id="{{id}}"  
                        data-bs-article-id="{{articleId}}"  
                        data-bs-nickname="{{nickname}}"  
                        data-bs-body="{{body}}"  
                >  
                    수정  
                </button>  
                <button type="button"  
                        class="btn btn-sm btn-outline-danger comment-delete-btn"  
                        data-bs-id="{{id}}">  
                    삭제  
                </button>  
            </div>  
            <div class="card-body">  
                {{body}}  
            </div>  
        </div>  
    {{/commentDtos}}  
</div>
<!-- modal 부분 생략 -->
  1. Modal 이벤트 처리를 구현한 <script> 코드 아래에 새 <script> 코드를 추가하여 삭제를 위한 REST API 호출을 fetch()로 작성한다.
    • 교재에선 script 역할 구분 상으로 따로 작성한 것으로 보인다.
<!-- _list.mustache -->
<div>
	<!-- 생략 -->
</div>
<!-- modal evnet -->
<script>
	// ...생략
</script>

<!-- delete event -->
<script>  
    // 삭제 버튼 전체 선택  
    const commentDeleteBtns = document.querySelectorAll(".comment-delete-btn");  
    // 모든 삭제 버튼에 이벤트 리스너 추가  
    commentDeleteBtns.forEach(btn => {  
        btn.addEventListener("click", function(event) {  
            // 이벤트 발생 요소 가져오기  
            const commentDeleteBtn = event.target;  
            // 삭제 대상 id 가져오기  
            const id = commentDeleteBtn.getAttribute("data-bs-id");  
  
            // 삭제 재확인  
            if (!confirm(`${id}번 댓글을 삭제하시겠습니까?`)) {  
                return; // 취소 시 삭제 동작 중단  
            }  
  
            // url 생성  
            const url = `/api/comments/${id}`;  
            fetch(url, {  
                method: "DELETE" // 삭제 요청을 위한 DELETE  
            }).then(res => {  
                if (!res.ok) { // 삭제 실패 시 처리  
                    alert("댓글 삭제 실패");  
                    return;  
                }  
  
                // 댓글 삭제 알림  
                const target = document.querySelector(`#comments-${id}`);  
                target.remove(); // 댓글을 View에서도 삭제  
                alert(`${id}번 댓글을 삭제했습니다`);  
                window.location.reload(); // 새로고침  
            });  
        });  
    });  
</script>

jpa_comment_view 11.png

jpa_comment_view 12.png

jpa_comment_view 13.png

jpa_comment_view 14.png